'''
Created on 20/08/2013

@author: Jesper
'''

import email.Utils
import email.Header
import re
import time

try:
    from hashlib import md5
except ImportError:
    from md5 import md5

import DateTimeConverter

_submoduleListForVersioning = [DateTimeConverter]
import versioning; _revisionInfo = versioning.getRevisionInfoFromTagsAndSubModules('$Revision: 113 $', '$Date: 2013-08-21 20:42:09 +0000 (Wed, 21 Aug 2013) $', _submoduleListForVersioning)


def _md5HexDigest(anyString):
    '''
    Return the MD5 hex digest of the given string.
    
    :param anyString:
        The string.
    :type anyString:
        String.
        
    Doctests:
    >>> _md5HexDigest("hello")
    '5d41402abc4b2a76b9719d911017c592'
    >>> _md5HexDigest("")
    'd41d8cd98f00b204e9800998ecf8427e'
    >>> _md5HexDigest(" ")
    '7215ee9c7d9dc229d2921a40e899ec5f'
    >>> _md5HexDigest("  ")
    '23b58def11b45727d3351702515f86af'
    >>> _md5HexDigest("Norwegian Blue")
    'f038cdb19e7449ffb95b3166f6a418cf'
    '''
    return md5(anyString).hexdigest()

def _makeMessageIdAsciiOnly(anyString):

    # JH: This looks like a bug to me: The given string is converted
    # to pure ascii in a non-bijective (non-one-to-one) way, thereby possibly
    # losing information. Therefore, two strings can be reduced to the same 
    # string, and therefore, two or more IDs can collide. This means that some 
    # emails won't be backed up, which is quite serious for a backup program.
    # If Ascii is needed, a better way is to convert the unicode in a safe 
    # and bijective way to an ascii sequence. This could for instance be 
    # Base64, UTF-7 or similar.
    if isinstance(anyString, unicode):
        return anyString.encode('ascii', 'replace')
    else:
        return anyString.decode('ascii', 'replace').encode('ascii', 'replace')

def _removeAngleBrackets(anyString):
    '''
    Return the given string with initial and ending angle brackets removed, 
    if any.

    Doctests:
    >>> _removeAngleBrackets('<testing 1>')
    'testing 1'
    >>> _removeAngleBrackets('<testing 2')
    'testing 2'
    >>> _removeAngleBrackets('testing 3>')
    'testing 3'
    >>> _removeAngleBrackets('testing 4')
    'testing 4'
    >>> _removeAngleBrackets(' <testing 5> ')
    ' <testing 5> '
    '''
    return anyString.lstrip('<').rstrip('>')

def _getMessageObjectFromMessageAsString(messageAsString):
    '''Wrapping of email.message_from_string().'''
    messageObject = email.message_from_string(messageAsString)
    return messageObject

def _getInternalMessageIdFromMessageObject(messageObject):
    internalMessageIdPossiblyWrappedInAngleBrackets = messageObject['Message-Id']
    if internalMessageIdPossiblyWrappedInAngleBrackets:
        # There was a internal messageId in the message.
        internalMessageId = _removeAngleBrackets(internalMessageIdPossiblyWrappedInAngleBrackets)  # Remove any wrapping "<" and ">" around the id. 
        internalMessageIdAsciiOnly = _makeMessageIdAsciiOnly(internalMessageId)  # JH: TODO: This could possibly make two different messageIds collide.
    else:
        # No Message-Id field or the field has no value.
        internalMessageIdAsciiOnly = None
    return internalMessageIdAsciiOnly


def makeMD5DigestFromMessageObject(messageObject):

    # JH: Note that this makes all messages different if they are 
    # checked by an antivirus that sets the X-Antivirus header 
    # with a date, for instance Avast.
    # TODO: So maybe all X-Antivirus* headers should be removed from 
    # the string before creating the digest.
    return _md5HexDigest(messageObject.as_string())

def _createSyntheticMessageIdFromMessageObject(messageObject):
    '''Return a synthetic messageId created from the given messageObject.'''
    sender = messageObject['From']
    subject = messageObject['Subject']
    
    messageDigest = makeMD5DigestFromMessageObject(messageObject)

    messageId = '%s.%s.%s' % (sender, subject, messageDigest)
    messageIdAsciiOnly = _makeMessageIdAsciiOnly(messageId) 
    return messageIdAsciiOnly


def _getMessageIdFromMessageObject(messageObject):
    '''Return either a modified version of the internal Message-Id of the 
    message, or a synthetic one if there is no internal Message-Id in the message.'''
    internalMessageIdAsciiOnly = _getInternalMessageIdFromMessageObject(messageObject)
    
    if internalMessageIdAsciiOnly:
        # There was a internal messageId in the message.
        # We will use that.
        messageIdAsciiOnly = internalMessageIdAsciiOnly
    else:
        # Create a synthetic messageId.
        messageIdAsciiOnly = _createSyntheticMessageIdFromMessageObject(messageObject)

    return messageIdAsciiOnly


def _getMessageIdFromMessageAsString(messageAsString):
    messageObject = _getMessageObjectFromMessageAsString(messageAsString)
    messageId = _getMessageIdFromMessageObject(messageObject)
    return messageId

def getDateTimeTupleFromMessageAsString(messageAsString):
    messageObject = _getMessageObjectFromMessageAsString(messageAsString)
    dateTimeTuple = _getMessageDateAsDateTimeTuple(messageObject)
    return dateTimeTuple

def getImap4InternalDateStringFromMessageObject(messageObject):
    messageDateAsDateTimeTuple = _getMessageDateAsDateTimeTuple(messageObject)
    imap4InternalDateString = DateTimeConverter.getImap4InternalDateStringFromDateTimeTuple(messageDateAsDateTimeTuple)
    return imap4InternalDateString

def getImap4InternalDateStringFromMessageAsString(messageAsString):
    '''Return the date of the message in IMAP4 INTERNALDATE representation, 
    i.e. a string in the form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"' (including the double quotes).'''
    messageObject = _getMessageObjectFromMessageAsString(messageAsString)
    imap4InternalDateString = getImap4InternalDateStringFromMessageObject(messageObject)
    return imap4InternalDateString


def _decodeHeaderToUnicode(headerString):
    try:
        decodedPartInUnicodeList = []
        for partString, charset in email.Header.decode_header(headerString):
            if charset is not None:
                # The partString is encoded in the given character set charset.
                try: 
                    decodedPartInUnicode = partString.decode(charset)
                except (UnicodeDecodeError, LookupError):
                    decodedPartInUnicode = partString.decode('ascii', 'replace')
            else:
                # The partString is not encoded.
                decodedPartInUnicode = partString.decode('ascii', 'replace')
            decodedPartInUnicodeList.append(decodedPartInUnicode)
        decodedHeaderInUnicode = ''.join(decodedPartInUnicodeList)
        return decodedHeaderInUnicode
    except:
        return _('<unparseable header>')

def _getHeaderFromMessageObjectInUnicode(messageObject, headerName):
    return _decodeHeaderToUnicode(messageObject[headerName])


def _getSenderFromFromField(decodedUnicodeFromField):
    '''
    Return a possibly modified version of the 'From' field
    from an email message, with an attempt to just return the
    email address if possible. If not possible, the whole
    'From' field is returned unchanged.
    
    :param decodedUnicodeFromField:
        The 'From' field from an email message, decoded and 
        in unicode format.
        
    Doctests:
        >>> _getSenderFromFromField("<a@b.com>")
        'a@b.com'
        >>> _getSenderFromFromField("<andy+beacon@ab.com>")
        'andy+beacon@ab.com'
        >>> _getSenderFromFromField("a@b.com")
        'a@b.com'
        >>> _getSenderFromFromField("Andy Beacon <a@b.com>")
        'a@b.com'
        >>> _getSenderFromFromField("Andy Beacon <@b.com>")
        'Andy Beacon <@b.com>'
        >>> _getSenderFromFromField("Andy Beacon <@.com>")
        'Andy Beacon <@.com>'
        >>> _getSenderFromFromField("Andy Beacon <a@b.>")
        'Andy Beacon <a@b.>'
        >>> _getSenderFromFromField("Andy Beacon")
        'Andy Beacon'
        >>> _getSenderFromFromField("Andy Beacon [a@b.com]")
        'Andy Beacon [a@b.com]'
        >>> _getSenderFromFromField("")
        ''
    '''
    match = re.match(r"^.*<(.+@.+\..+)>.*$", decodedUnicodeFromField)
    if match:
        sender = match.group(1) # Use only the apparent email address between the "<" and ">" (and not the name).
    else:
        sender = decodedUnicodeFromField # Use the "From" field unchanged. It might be in any format.
    return sender

def _getSenderFromMessageObject(messageObject):
    decodedUnicodeFromField = _getHeaderFromMessageObjectInUnicode(messageObject, 'From')
    sender = _getSenderFromFromField(decodedUnicodeFromField)
    return sender

def _getSubjectFromMessageObject(messageObject):
    subject = _getHeaderFromMessageObjectInUnicode(messageObject, 'Subject')
    return subject

def _getSenderAndSubjectFromMessageObject(messageObject):
    '''Return a 2-tuple with a possibly modified "From" field 
    (only the email address if the email address is identifiable) 
    and subject for the given parsed email message. 
    
    Both returned strings are in plain unicode.
    '''

    messageSender = _getSenderFromMessageObject(messageObject)
    messageSubject = _getSubjectFromMessageObject(messageObject)
    
    return messageSender, messageSubject


def _getSenderAndSubjectFromMessageAsString(messageAsString):
    '''Return a 2-tuple with a possibly modified "From" field 
    (only the email address if the email address is identifiable) 
    and subject for the given unparsed email message. 
    
    Both returned strings are in plain unicode.
    '''
    messageObject = _getMessageObjectFromMessageAsString(messageAsString)
    return _getSenderAndSubjectFromMessageObject(messageObject)


def _makeDateTimeTupleAcceptable(dateTimeTuple):
    try:
        # We will try to convert the dateTimeTuple in local time
        # into seconds since the Unix Epoch and see if it succeeds.
        # If it does, it is a valid datetime.
        secondsSinceTheEpoch = time.mktime(dateTimeTuple)
    except OverflowError: 
        # Overflow occurred, meaning the dateTimeTuple was not a valid
        # local time according to time.mktime().
        # We will fall back to the Unix Epoch (January 1st, 1970).
        secondsSinceTheEpoch = 0

    # Now create a real time.struct_time() object.
    resultingDateTimeTuple = time.localtime(secondsSinceTheEpoch)

    return resultingDateTimeTuple

def _trimDateTimeTuple(dateTimeTuple):
    '''Return a valid time.struct_time (a named dateTimeTuple) from a 
    possibly adjusted version of the given dateTimeTuple in local time. 
    The returned date can be safely converted to seconds since the Unix Epoch
    and back again. If the given date cannot be converted to seconds since the 
    Unix Epoch, the Unix Epoch is returned. If the date is after the year 9999,
    a date in the year 9999 is returned.
    
    Doctests:

        Valid dates:
        >>> _trimDateTimeTuple((1969, 0, 0, -1, 65, 63, 0, 0, 0))
        time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=1, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)
        >>> _trimDateTimeTuple((2113, 0, 0, -1, 65, 63, 2, 317, 0))
        time.struct_time(tm_year=2113, tm_mon=1, tm_mday=1, tm_hour=0, tm_min=59, tm_sec=59, tm_wday=6, tm_yday=1, tm_isdst=0)
        >>> _trimDateTimeTuple((2113, 12, 31, 23, 59, 62, 2, 317, 0))
        time.struct_time(tm_year=2113, tm_mon=12, tm_mday=31, tm_hour=23, tm_min=59, tm_sec=59, tm_wday=6, tm_yday=365, tm_isdst=0)

        Year 3000 seems to be the last valid year:
        >>> _trimDateTimeTuple((3000, 12, 31, 23, 59, 59, 0, 0, 0))
        time.struct_time(tm_year=3000, tm_mon=12, tm_mday=31, tm_hour=23, tm_min=59, tm_sec=59, tm_wday=2, tm_yday=365, tm_isdst=0)

        Invalid dates after year 3000 results in the Unix Epoch:
        >>> _trimDateTimeTuple((3001, 01, 01, 23, 59, 59, 0, 0, 0))
        time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=1, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)
        >>> _trimDateTimeTuple((3999, 12, 31, 23, 59, 59, 0, 0, 0))
        time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=1, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)
        >>> _trimDateTimeTuple((9999, 12, 31, 23, 59, 59, 0, 0, 0))
        time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=1, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)
        >>> _trimDateTimeTuple((10113, 12, 31, 23, 59, 59, 0, 0, 0))
        time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=1, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)
    '''
    
    def trim(minimum, value, maximum):
        '''Return `value` bounded to the interval from `minimum` to `maximum`.
        
        Doctests:
            >>> trim(0, 0, 0)
            0
            >>> trim(0, 0, 59)
            0
            >>> trim(1, 0, 31)
            1
            >>> trim(0, -1, 59)
            0
            >>> trim(0, 59, 59)
            59
            >>> trim(0, 60, 59)
            59
        '''
        if value < minimum:
            return minimum
        elif value > maximum:
            return maximum
        else:
            return value
    
    (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst) = dateTimeTuple

    tm_year = trim(1970, tm_year, 9999)
    tm_mon = trim(1, tm_mon, 12)
    tm_mday = trim(1, tm_mday, 31)
    tm_hour = trim(0, tm_hour, 23)
    tm_min = trim(0, tm_min, 59)
    tm_sec = trim(0, tm_sec, 59)
    tm_yday = trim(1, tm_yday, 366)    
    if tm_isdst not in [-1, 0, 1]:
        tm_isdst = -1
    
    dateTimeTuple = (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst)
    
    resultingDateTimeTuple = _makeDateTimeTupleAcceptable(dateTimeTuple)

    return resultingDateTimeTuple 

def _getMessageDateAsDateTimeTuple(messageObject):
    parsedDateTimeTuple = email.Utils.parsedate(messageObject['Date'])
    
    if parsedDateTimeTuple is not None:
        # Date found.
        parsedAndTrimmedDateTimeTuple = _trimDateTimeTuple(parsedDateTimeTuple)
    else:
        # No date returned by email.Utils.parsedate().
        parsedAndTrimmedDateTimeTuple = time.localtime(0)  # Set to January 1st, 1970, the "Epoch". No need to trim it.

    return parsedAndTrimmedDateTimeTuple


def imap_unescape(escapedImapString):
    '''
    
    :param escapedImapString:
        An escaped IMAP string.
    
    Doctests:
    
        One backslash followed by a double quote gives just the 
        double quote.
        >>> imap_unescape(r'\"')
        '"'
        
        Two backslashes gives one backslash. It looks strange here
        in the docstring, but that is because the docstring is a string in
        itself and therefore all backslashes has to be escaped here.
        >>> imap_unescape(r'\\\\')
        '\\\\'

        Combination
        >>> imap_unescape(r'Hello!\\\\\" And hello again.')
        'Hello!\\\\" And hello again.'

    '''
    unescapedString = re.sub(r'\\([\\"])', r'\1', escapedImapString)
    return unescapedString


def imap_escape(unescapedString):
    escapedImapString = re.sub(r'([\\"])', r'\\\1', unescapedString)
    return escapedImapString


